Custom Error Types
Using String or &str as errors is:
- Hard to match on
- Easy to misuse
- Not expressive
Custom error types let you:
- Represent different failure cases clearly
- Attach data to errors
- Use
?cleanly with multiple error sources - Provide better debugging and user messages
Basic Custom Error Type with enum
Example 1: Simple calculator errors
use std::fmt;
#[derive(Debug)]
enum CalcError {
DivisionByZero,
NegativeNumber,
}
impl fmt::Display for CalcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CalcError::DivisionByZero => write!(f, "Cannot divide by zero"),
CalcError::NegativeNumber => write!(f, "Negative numbers are not allowed"),
}
}
}
impl std::error::Error for CalcError {}
fn divide(a: i32, b: i32) -> Result<i32, CalcError> {
if b == 0 {
Err(CalcError::DivisionByZero)
} else if a < 0 || b < 0 {
Err(CalcError::NegativeNumber)
} else {
Ok(a / b)
}
}
fn main() {
match divide(10, 0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}
- enum CalcError defines all possible error cases.
- Implements:
Debug→ for debugging output.Display→ for user-friendly messages.Error→ so it works with standard error handling tools.
- The function returns
Result<i32, CalcError>.
Adding underlying errors (wrapping errors)
Often, your function calls other functions that already return errors (like io::Error or ParseIntError). You want to wrap those instead of losing information.
Example 2: Wrapping I/O and parsing errors
use std::fs;
use std::io;
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
enum ReadNumberError {
Io(io::Error),
Parse(ParseIntError),
}
impl fmt::Display for ReadNumberError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReadNumberError::Io(e) => write!(f, "I/O error: {}", e),
ReadNumberError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for ReadNumberError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ReadNumberError::Io(e) => Some(e),
ReadNumberError::Parse(e) => Some(e),
}
}
}
// Enable automatic conversion using `?`
impl From<io::Error> for ReadNumberError {
fn from(err: io::Error) -> Self {
ReadNumberError::Io(err)
}
}
impl From<ParseIntError> for ReadNumberError {
fn from(err: ParseIntError) -> Self {
ReadNumberError::Parse(err)
}
}
fn read_number(path: &str) -> Result<i32, ReadNumberError> {
let contents = fs::read_to_string(path)?; // io::Error → ReadNumberError
let number = contents.trim().parse::<i32>()?; // ParseIntError → ReadNumberError
Ok(number)
}
fn main() {
match read_number("number.txt") {
Ok(n) => println!("Number: {}", n),
Err(e) => {
println!("Error: {}", e);
if let Some(source) = e.source() {
println!("Caused by: {}", source);
}
}
}
}
- The enum stores underlying errors.
Fromimplementations allow?to work seamlessly.source()enables error chaining (useful for logging).
Using thiserror (recommended for real projects)
Manually implementing Display, Error, and From can get verbose. The thiserror crate automates this.
Example 3: Same error using thiserror
use thiserror::Error;
use std::io;
use std::num::ParseIntError;
#[derive(Debug, Error)]
enum ReadNumberError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Failed to parse number: {0}")]
Parse(#[from] ParseIntError),
}
fn read_number(path: &str) -> Result<i32, ReadNumberError> {
let contents = std::fs::read_to_string(path)?;
let number = contents.trim().parse::<i32>()?;
Ok(number)
}
#[derive(Error)]generates:DisplayErrorFromautomatically.
- Much cleaner and more maintainable.
Designing good custom errors
Good error types:
- Are specific (not just
UnknownError) - Use enums for multiple cases
- Wrap underlying errors instead of discarding them
- Provide helpful
Displaymessages
Library vs Application Errors
Libraries:
- Use precise custom error enums
- Let callers match on variants
Applications:
- Often use
Box<dyn Error>oranyhow::Errorat top-level - Internally still benefit from structured errors
Summary
- Custom error types improve clarity, safety, and debugging.
- Use
enumto represent failure cases. - Implement
Display,Error, andFromfor usability. - Use
thiserrorto reduce boilerplate. - Combine with
?for clean, composable error propagation.